﻿using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using DynamicConfigurationManager.ConfigMapAttributes;

namespace DynamicConfigurationManager
{
    /// <summary>
    /// </summary>
    public class DynamicConfigurationSectionHandler : IConfigurationSectionHandler
    {
        private readonly HashSet<string> avoidRepeatCache = new HashSet<string>( StringComparer.InvariantCultureIgnoreCase );
        private readonly ConfigMapHandler configMapHandler = new ConfigMapHandler(); // Handles ConfigMap parsing
        private readonly Dictionary<string, string> settings = new Dictionary<string, string>( StringComparer.InvariantCultureIgnoreCase );

        private int numOfHandledConfigMaps; // Count of handled ConfigMaps, if 0 then throw exception

        #region IConfigurationSectionHandler Members

        /// <summary>
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="configContext"></param>
        /// <param name="section"></param>
        /// <returns></returns>
        /// <exception cref="ConfigurationErrorsException"></exception>
        public object Create( object parent, object configContext, XmlNode section )
        {
            // Throw error if null section
            if( section == null )
            {
                throw new ConfigurationErrorsException( Resources.SectionHandlerNodeNull );
            }

            // Parse the config to build up the settings
            ParseConfig( section );

            // Check to see if there are any "configSet={setname}" arguments on the command line, and map those value in too
            ProcessCommandLineArgs( section );

            // Check if configMaps found
            if( numOfHandledConfigMaps == 0 )
            {
                throw new ConfigurationErrorsException( string.Format( Resources.NoConfigMapsFound, LogHelper.Entry ) );
            }

            // perform variable substitutions
            SubstituteVariables();

            // copy the dynamic settings to the appSettings global and export the settings to the environment with the "DCM" prefix for sub-process consumption
            MergeToAppSettingsAndExport();

            return settings;
        }

        #endregion IConfigurationSectionHandler Members

        // Check to see if there are any "configSet={setname}" arguments on the command line
        private void ProcessCommandLineArgs( XmlNode section )
        {
            foreach( var configSet in
                Environment.GetCommandLineArgs()
                    .Where( arg => arg.StartsWith( "configSet=", StringComparison.OrdinalIgnoreCase ) )
                    .Select( arg => arg.Substring( arg.IndexOf( "=", StringComparison.Ordinal ) + 1 ) )
                    .Select( setName => section.SelectSingleNode( "configSet[@name=\"" + setName + "\"]" ) )
                    .Where( configSet => configSet != null ) )
            {
                ParseConfig( configSet );
                ++numOfHandledConfigMaps;
            }
        }

        // perform string substitutions of $(keyname) to their configuration value
        // i.e. process variables like
        // <add key="pingHost" value="localhost"/>
        // <add key="Arguments" value="-n 5 -w 100 $(pingHost)"/>
        private void SubstituteVariables()
        {
            var re = new Regex( @"\$\(([^\)]+)\)", RegexOptions.IgnoreCase );
            // queue up the changes, as we cannot change a Dictionary while iterating over it
            var updateQueue = new List<Tuple<string, string>>();

            foreach( var kvp in settings )
            {
                if( re.IsMatch( kvp.Value ) )
                {
                    var result = kvp.Value;
                    foreach( Match m in re.Matches( kvp.Value ) )
                    {
                        var outer = m.Groups[0].Value; // i.e. $(HostName)
                        var inner = m.Groups[1].Value; // i.e HostName
                        if( settings[inner] != null )
                        {
                            result = result.Replace( outer, settings[inner] );
                        }
                    }
                    updateQueue.Add( new Tuple<string, string>( kvp.Key, result ) );
                }
            }

            foreach( var kvp in updateQueue )
            {
                AddSetting( kvp.Item1, kvp.Item2 );
            }
        }

        // copy the dynamic settings to the appSettings global and export the settings to the environment with the "DCM" prefix for sub-process consumption
        private void MergeToAppSettingsAndExport()
        {
            var appSettings = ConfigurationManager.AppSettings;
            foreach( var kvp in settings )
            {
                appSettings[kvp.Key] = kvp.Value;
                Environment.SetEnvironmentVariable( "DCM" + kvp.Key, kvp.Value, EnvironmentVariableTarget.Process );
            }
        }

        private void ParseConfig( XmlNode currentNode )
        {
            // for each child of the current node, call the relevant processing step
            foreach( XmlNode node in currentNode.ChildNodes )
            {
                switch( node.Name.ToLower() )
                {
                    case "configmap":
                        if( ParseConfigMap( node ) )
                        {
                            // break out of loop once a successful match is found with stopOnMatch="true"
                            var stopOnMatch = node.Attributes?["stopOnMatch"];
                            if( stopOnMatch != null )
                            {
                                bool shouldStop;
                                if( bool.TryParse( stopOnMatch.Value, out shouldStop ) )
                                {
                                    if( shouldStop )
                                        return;
                                }
                            }
                        }
                        break;

                    case "add":
                        ParseAddNode( node );
                        break;

                    case "include":
                    case "includeset":
                        ParseIncludeSet( node );
                        break;

                    // capability to include external files
                    case "includefile":
                        ParseIncludeFile( node );
                        break;

                    case "includedb":
                        ParseIncludeDb( node );
                        break;

                    case "includeregistry":
                        ParseIncludeRegistry( node );
                        break;

                        // TODO: implement capability to create xmlFragment values, and deserialize as objects
                }
            }
        }

        private bool ParseConfigMap( XmlNode currentNode )
        {
            var successful = configMapHandler.IsHandled( currentNode );

            // check if we want to include the children of this configMap
            if( successful )
            {
                // Increment the number of handled configMaps
                ++numOfHandledConfigMaps;

                // This is recursive, so call parseConfig
                ParseConfig( currentNode );
            }

            return successful;
        }

        private void ParseIncludeSet( XmlNode currentNode )
        {
            var setName = currentNode.Attributes?["set"];
            if( setName == null )
            {
                return;
            }

            LogHelper.Append( "Adding Set: {0}", setName.Value );

            // find the configSet specified - must have the same parent as the parent of this include node
            var configSet = currentNode.SelectSingleNode( "../../configSet[@name=\"" + setName.Value + "\"]" );
            if( configSet != null )
            {
                ParseConfig( configSet );
            }
        }

        private void ParseIncludeFile( XmlNode currentNode )
        {
            var path = currentNode.Attributes?["path"];
            if( path == null )
            {
                return;
            }

            // build absolute path from main config file's path
            // locate the config file's path
            var mainDir = Path.GetDirectoryName( AppDomain.CurrentDomain.SetupInformation.ConfigurationFile );
            if( mainDir == null )
            {
                return;
            }

            var includeFile = Path.Combine( mainDir, path.Value );

            // track which files we open and don't open it again (avoid recursion)
            if( avoidRepeatCache.Contains( includeFile ) )
            {
                LogHelper.Append( "Already processed file: {0}", path.Value );
                return;
            }
            avoidRepeatCache.Add( includeFile );

            if( !File.Exists( includeFile ) )
            {
                LogHelper.Append( "File does not exist: {0}", path.Value );
                return;
            }

            LogHelper.Append( "Retrieving configuration settings from file: {0}", path.Value );

            // open the file and include it
            var xdoc = new XmlDocument();
            xdoc.Load( includeFile );

            var config = xdoc.DocumentElement;
            if( config != null )
            {
                ParseConfig( config );
            }
        }

        // <includeDb cxAlias="testDbAlias" query="select key, value from AppSettings where env='$(myEnv)'" />
        // cxAlias = config db alias to a connection string
        private void ParseIncludeDb( XmlNode currentNode )
        {
            // Check to see if there is a configuration database alias identified
            var dbAlias = currentNode.Attributes?["dbAlias"];
            if( dbAlias == null )
            {
                return;
            }

            // Check to see if there is a configuration database alias identified
            var query = currentNode.Attributes["query"];
            if( query == null )
            {
                return;
            }

            if( avoidRepeatCache.Contains( dbAlias.Value + query.Value ) )
            {
                LogHelper.Append( "Already processed database: {0} {1}", dbAlias.Value, query.Value );
                return;
            }
            avoidRepeatCache.Add( dbAlias.Value + query.Value );

            // get key/value pairs from db and add them directly
            try
            {
                var cxSetting = ConfigurationManager.ConnectionStrings[dbAlias.Value];
                if( cxSetting == null )
                {
                    return;
                }

                // we have a config db! So connect to it and query for any values
                LogHelper.Append( "Retrieving configuration settings from db: {0} {1}", cxSetting.ConnectionString, query );
                using( var cx = new SqlConnection( cxSetting.ConnectionString ) )
                {
                    using( var cmd = cx.CreateCommand() )
                    {
                        cmd.CommandText = query.Value;
                        cmd.CommandType = CommandType.Text;
                        cmd.CommandTimeout = 30;

                        LogHelper.Append( "Retrieving configuration settings" );

                        cx.Open();
                        using( var dr = cmd.ExecuteReader( CommandBehavior.CloseConnection | CommandBehavior.SingleResult ) )
                        {
                            // iterate over the results and add them
                            while( dr.Read() )
                            {
                                var key = dr[0].ToString();
                                var value = dr[1].ToString();
                                AddSetting( key, value );
                            }
                            dr.Close();
                        }
                    }
                }
            }
            catch( Exception ex )
            {
                LogHelper.Append( "Exception reading database values: {0}", ex.Message );
            }
        }

        private void ParseIncludeRegistry( XmlNode currentNode )
        {
            var keyName = currentNode.Attributes?["HKLMPath"];
            if( keyName == null )
            {
                return;
            }

            LogHelper.Append( "Adding settings from Registry Key: {0}", keyName.Value );

            try
            {
                // enumerate the registry key contents
                var rk = Registry.LocalMachine.OpenSubKey( keyName.Value );
                if( rk != null )
                {
                    foreach( var key in rk.GetValueNames() )
                    {
                        var value = Convert.ToString( rk.GetValue( key ) );
                        AddSetting( key, value );
                    }
                }
            }
            catch( Exception ex )
            {
                LogHelper.Append( "Failure reading registry key: '{0}' {1}", keyName.Value, ex.Message );
            }
        }

        private void ParseAddNode( XmlNode newNode )
        {
            var xKey = newNode.Attributes?["key"];
            if( string.IsNullOrWhiteSpace( xKey?.Value ) )
            {
                return;
            }

            var xValue = newNode.Attributes["value"];
            if( xValue == null )
            {
                return;
            }

            AddSetting( xKey.Value, xValue.Value );
        }

        private void AddSetting( string key, string value )
        {
            // check to see if we already have an item with the same key
            if( settings.ContainsKey( key ) )
            {
                // found an item with the same key, so replace the value with the new value
                settings[key] = value;
                LogHelper.Append( "Replaced: {0} = {1}", key, value );
            }
            else
            {
                // not found already, so add it
                settings.Add( key, value );
                LogHelper.Append( "Added: {0} = {1}", key, value );
            }
        }
    }
}